#region License

// This file is part of Grawlix.
// 
// Grawlix is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of
// the License, or (at your option) any later version.
// Grawlix is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public
// License along with Grawlix.  If not, see
// <http://www.gnu.org/licenses/>.

#endregion

using System;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Drawing.Imaging;
using Grawlix.Geometry;
using Grawlix.Services.Resources;
using OpenTK.Graphics;
using PixelFormat=System.Drawing.Imaging.PixelFormat;

namespace Grawlix.Services.Rendering
{
	/// <summary>
	/// The texture state information for a loaded texture.
	/// </summary>
	[Serializable]
	internal sealed class GLTextureState : ITextureState
	{
		/// <summary>
		/// Whether the texture has been unloaded.
		/// </summary>
		private bool _disposed;

		/// <summary>
		/// The texture identifier.
		/// </summary>
		private uint _id;

		/// <summary>
		/// The inverted dimensions of the texture.
		/// </summary>
		private Vector2 _inverseDimensions = new Vector2(1, 1);

		/// <summary>
		/// Initializes a new instance of the 
		/// <see cref="GLTextureState"/> class.
		/// </summary>
		/// <param name="image">The image to load.</param>
		/// <exception cref="ArgumentNullException"><c>image</c> is
		/// null.</exception>
		public GLTextureState(WrappedImage image)
		{
			if (image == null)
				throw new ArgumentNullException("image");
			Load(image.ImageData);
		}

		/// <summary>
		/// Gets the dimensions of the texture.
		/// </summary>
		/// <value>The dimensions of the texture.</value>
		public Vector2 Dimensions
		{
			get { return 1 / _inverseDimensions; }
		}

		/// <summary>
		/// Binds the texture so that rendering operations will use it.
		/// </summary>
		/// <exception cref="ObjectDisposedException">The object has
		/// already been disposed of.</exception>
		internal void Bind()
		{
			if (_disposed)
				throw new ObjectDisposedException("GLTextureState");
			GL.BindTexture(TextureTarget.Texture2D, _id);
			GL.MatrixMode(MatrixMode.Texture);
			GL.LoadIdentity();
			GL.Scale(_inverseDimensions.X, _inverseDimensions.Y,
			         (double)1);
		}

		/// <summary>
		/// Loads the specified image.
		/// </summary>
		/// <param name="image">The image to load.</param>
		[SuppressMessage("Microsoft.Security", "CA2122",
			Justification = "The functions are not directly exposed."
			)]
		private void Load(Image image)
		{
			int width = ValidDimension(image.Width);
			int height = ValidDimension(image.Height);
			var bitmap = image as Bitmap;
			if (((bitmap == null) || (width != image.Width)) ||
			    (height != image.Height))
				bitmap = new Bitmap(image, width, height);
			Vector2 inverseDimensions = 1 /
			                            new Vector2(image.Width,
			                                        image.Height);
			uint id;
			GL.GenTextures(1, out id);
			GL.BindTexture(TextureTarget.Texture2D, id);
			var lockRect = new Rectangle(0, 0, bitmap.Width,
			                             bitmap.Height);
			const ImageLockMode mode = ImageLockMode.ReadOnly;
			const PixelFormat format = PixelFormat.Format32bppArgb;
			BitmapData data = bitmap.LockBits(lockRect, mode, format);
			GL.TexImage2D(TextureTarget.Texture2D, 0,
			              PixelInternalFormat.Rgba, data.Width,
			              data.Height, 0,
			              OpenTK.Graphics.PixelFormat.Bgra,
			              PixelType.UnsignedByte, data.Scan0);
			bitmap.UnlockBits(data);
			GL.TexParameter(TextureTarget.Texture2D,
			                TextureParameterName.TextureMinFilter,
			                0x2601);
			GL.TexParameter(TextureTarget.Texture2D,
			                TextureParameterName.TextureMagFilter,
			                0x2601);
			_inverseDimensions = inverseDimensions;
			_id = id;
		}

		/// <summary>
		/// Resets the state to default values.
		/// </summary>
		internal void Reset()
		{
			_id = 0;
			_inverseDimensions = new Vector2(1, 1);
		}

		/// <summary>
		/// Unloads the texture state.
		/// </summary>
		/// <exception cref="ObjectDisposedException">The object has
		/// already been disposed of.</exception>
		internal void Unload()
		{
			if (_disposed)
				throw new ObjectDisposedException("GLTextureState");
			_disposed = true;
			GL.DeleteTextures(1, ref _id);
			Reset();
		}

		/// <summary>
		/// Finds a valid <c>OpenGL</c> dimension near those specified.
		/// </summary>
		/// <param name="dimension">The image dimension.</param>
		/// <returns>A valid dimension for an <c>OpenGL</c> texture.
		/// </returns>
		private static int ValidDimension(int dimension)
		{
			if (dimension < 0x40)
				return 0x40;
			if (dimension > 0x100)
				return 0x100;
			if (((dimension & dimension) - 1) == 0)
				return dimension;
			int nextBest = 0x80;
			while (nextBest < dimension)
				nextBest *= 2;
			return nextBest;
		}
	}
}